package uk.ljmu.qudexapps.swfspreadsheet.services;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.binding.message.MessageBuilder;
import org.springframework.binding.message.MessageContext;
import org.springframework.stereotype.Service;
import org.springframework.webflow.execution.RequestContext;

import uk.ljmu.metadata.schemas.SchemaInstanceContainer;
import uk.ljmu.qudexapps.swfspreadsheet.domain.Element;
import uk.ljmu.qudexapps.swfspreadsheet.domain.Elements;
import uk.ljmu.qudexapps.swfspreadsheet.domain.MdSchema;
import uk.ljmu.qudexapps.swfspreadsheet.domain.MdSchemaInstance;
import uk.ljmu.qudexapps.swfspreadsheet.domain.MetadataFlowSchemas;
import uk.ljmu.qudexapps.swfspreadsheet.domain.VocabularyElement;
import uk.ljmu.qudexapps.swfspreadsheet.utilities.Constants;
import uk.ljmu.qudexapps.swfspreadsheet.utilities.RdfFunctions;

import com.hp.hpl.jena.rdf.model.Model;

@Service("mdService")
public class MetadataServiceImpl implements MetadataService {

	/**
	 * Generated Class UID
	 */
	private static final long serialVersionUID = -6778253694655906722L;

	/**
	 * Class logger instance
	 */
	private static Logger log = Logger.getLogger(MetadataServiceImpl.class);

	/**
	 * New session object to manage all the metadata schema related objects
	 * involved in a template creation flow (process)
	 */
	@Autowired
	private MetadataFlowSchemas mdFlow;

	/**
	 * Gets all the elements from a given metadata schema by using the online
	 * RDF version of the schema
	 * 
	 * @param schema
	 *            metadata schema URL
	 * @return list of VocabularyElement instances representing the metadata
	 *         elements
	 */
	public List<VocabularyElement> getMetadataFieldsFromSchema(String prefix) {

		int index = this.findSchemaInstance(prefix);

		if (index == -1) {
			List<VocabularyElement> listSchemaElems;
			Model auxModel = this.getDynamicSchema(prefix);

			listSchemaElems = RdfFunctions.readPropertiesFromSchema(auxModel);

			if (listSchemaElems == null || listSchemaElems.size() == 0) {
				// AMG New method to obtain Model offline, from
				// SchemaInstanceContainer class
				log.info("Getting Schema elements offline, static method");
				auxModel = SchemaInstanceContainer
						.findStaticSchemaModel(prefix);
				listSchemaElems = RdfFunctions
						.readPropertiesFromSchema(auxModel);
			} else {
				normalizeNames(listSchemaElems);
			}

			return listSchemaElems;
		} else {
			return this.mdFlow.getMdSchemaInstanceContainer().get(index)
					.getSelectedSchemaElements();
		}
	}

	/**
	 * Internal interface method to find a schema object by its namespace
	 * prefix, returning its index from the array of schema objects
	 * 
	 * @param ns
	 *            the schema object's namespace prefix
	 * 
	 * @return the schema object's index; -1 if not found
	 */
	private int findSchemaInstance(String ns) {
		int index = 0;
		for (MdSchemaInstance instance : this.mdFlow
				.getMdSchemaInstanceContainer()) {
			if (instance.getPrefix().equalsIgnoreCase(ns))
				return index;
			index++;
		}
		return -1;
	}

	/**
	 * Adds a new MdSchemaInstance object to the list of metadata schemas
	 * 
	 * @param mdSchemaInstance
	 *            the mdSchemaInstance to add
	 * @return true is the schema was added; false otherwise
	 */
	public boolean addMetadataSchema(MdSchemaInstance mdSchemaInstance) {
		boolean bFound = false;
		try {
			for (MdSchemaInstance instance : this.mdFlow
					.getMdSchemaInstanceContainer()) {
				if (instance.getPrefix().equalsIgnoreCase(
						mdSchemaInstance.getPrefix())) {
					bFound = true;
					if (!this.anyElementSelected(mdSchemaInstance
							.getSelectedSchemaElements()))
						instance.setRemoved(true);
					else {
						instance.setRemoved(false);
						instance.setSelectedSchemaElements(mdSchemaInstance
								.getSelectedSchemaElements());
					}
					break;
				}
			}
			if (!bFound)
				this.mdFlow.getMdSchemaInstanceContainer()
						.add(mdSchemaInstance);
		} catch (Exception ex) {
			return false;
		}
		return true;
	}

	/**
	 * Internal method to check if a given array of metadata schema elements
	 * presents any user-selected elements
	 * 
	 * @param selectedSchemaElements
	 *            the array of metadata elements to check
	 * 
	 * @return true if there are elements marked as user-selected; false
	 *         otherwise
	 */
	private boolean anyElementSelected(
			List<VocabularyElement> selectedSchemaElements) {

		for (VocabularyElement elem : selectedSchemaElements) {
			if (elem.isSelected())
				return true;
		}

		return false;
	}

	/**
	 * Deletes a particular MdSchemaInstance Object identified by its index in
	 * the array/list of schema instances
	 * 
	 * @param index
	 *            int index of the deleted object
	 * @return true if successfully deleted; false otherwise
	 */
	public boolean deleteMetadataSchema(int index) {
		try {
			this.mdFlow.getMdSchemaInstanceContainer().remove(index);
		} catch (Exception ex) {
			log.error(ex.getLocalizedMessage());
			return false;
		}

		return true;
	}

	/**
	 * Returns a new MdSchemaInstance Object
	 */
	public MdSchemaInstance createMdSchemaInstance() {
		return new MdSchemaInstance();
	}

	/**
	 * 
	 */
	public void initialiseQudexElements() {
		MdSchemaInstance auxSchemaInstance;

		if (!this.mdFlow.isSchemasPresent()) {
			try {
				initialiseQudexSpreadsheetElements();
				List<VocabularyElement> auxVocab = new ArrayList<VocabularyElement>();
				for (Element elem : this.mdFlow.getQudexElements().getElement()) {
					if (elem.getNs() == null || elem.getNs().equals(""))
						continue;
					int index = findSchemaInstance(elem.getNs());
					if (index > -1)
						auxSchemaInstance = this.mdFlow
								.getMdSchemaInstanceContainer().get(index);
					else {
						auxSchemaInstance = new MdSchemaInstance();
						auxSchemaInstance.setPrefix(elem.getNs());
						auxSchemaInstance
								.setReadableName(Constants.readableNames
										.get(elem.getNs()));
						this.mdFlow.getMdSchemaInstanceContainer().add(
								auxSchemaInstance);
						auxVocab = RdfFunctions.readPropertiesFromSchema(this
								.getDynamicSchema(elem.getNs()));
						auxSchemaInstance.setSelectedSchemaElements(auxVocab);
					}
					auxSchemaInstance.setRemoved(false);
					VocabularyElement vElem = auxSchemaInstance
							.getElementFromSelectedSchemas(elem.getName());
					if (vElem != null) {
						vElem.setOptional(elem.isOptional());
						vElem.setSelected(true);
						// AMG Set it hidden initially and then in
						// metadata-element-flow then set the ones should be
						// active
						// for each type of template to false (not hidden)
						vElem.setHidden(true);
					}
				}
			} catch (Exception ex) {
				log.error(ex.getLocalizedMessage());
			}
		}
	}

	/**
	 * Deletes the selected Metadata Schema Instances
	 * 
	 * @param list
	 *            of metadata prefixes to delete from the selected schemas
	 */
	public void elimGroup(String list, MessageContext context) {
		if (list != null) {
			boolean valid = true;
			String[] group = list.split(";");

			if (group.length > 0 && group[0] != "")
				for (String deletedElement : group) {
					int index = Integer.parseInt(deletedElement);

					MdSchemaInstance instance = this.mdFlow
							.getMdSchemaInstanceContainer().get(index);

					for (VocabularyElement selectedElem : instance
							.getSelectedSchemaElements()) {
						if (!selectedElem.isOptional()) {
							valid = false;
						} else
							selectedElem.setSelected(false);
					}
					if (valid)
						this.mdFlow.getMdSchemaInstanceContainer().get(index)
								.setRemoved(true);
					else {
						context.addMessage(new MessageBuilder().error()
								.code("compulsoryElements").build());
					}
				}
		}
	}

	/**
	 * 
	 * @param listSchemaElems
	 */
	@SuppressWarnings("unused")
	private void addQudexInfoToElements(List<VocabularyElement> listSchemaElems) {
		for (VocabularyElement elem : listSchemaElems) {
			for (Element qudexElem : this.mdFlow.getQudexElements()
					.getElement()) {
				if (elem.getName().equalsIgnoreCase(qudexElem.getName())) {
					elem.setOptional(qudexElem.isOptional());
					elem.setSelected(true);
					break;
				}
			}
		}
	}

	/**
	 * 
	 * @param prefix
	 * @return
	 */
	private Model getDynamicSchema(String prefix) {
		for (MdSchema schema : this.mdFlow.getAvailableSchemas()) {
			if (schema.getPrefix().equalsIgnoreCase(prefix))
				return schema.getSchemaModel();
		}

		return null;
	}

	/**
	 * Transforms all the metadata element names to lowercase
	 * 
	 * @param listSchemaElems
	 *            list of metadata element names
	 */
	private void normalizeNames(List<VocabularyElement> listSchemaElems) {
		for (VocabularyElement elem : listSchemaElems) {
			elem.setLabel(elem.getLabel().toLowerCase());
		}
	}

	public boolean initialiseQudexSpreadsheetElements() throws IOException {
		ClassLoader classLoader = Thread.currentThread()
				.getContextClassLoader();
		InputStream is = null;
		JAXBContext context;

		try {
			if (this.mdFlow.getTypeSS() == Constants.Q_COLLECTIONS_SS) {
				is = classLoader
						.getResourceAsStream("schemas/qudex-collection.xml");
			} else {
				is = classLoader
						.getResourceAsStream("schemas/qudex-document.xml");
			}
			context = JAXBContext.newInstance(Elements.class);
			Unmarshaller unmarshaller = context.createUnmarshaller();
			// validate against schema, note: setting schema to null will turn
			// validator off
			unmarshaller.setSchema(null);
			this.mdFlow.setQudexElements(Elements.class.cast(unmarshaller
					.unmarshal(is)));

			return true;
		} catch (JAXBException e) {
			log.fatal(e.getMessage());
			return false;
		} finally {
			if (is != null)
				is.close();
		}
	}

	/**
	 * @param schemasPresent
	 *            the schemasPresent to set
	 */
	public void setSchemasPresent(boolean schemasPresent) {
		this.mdFlow.setSchemasPresent(schemasPresent);
	}

	/**
	 * Controller utility method. Close the current session
	 * 
	 * @param context
	 *            the RequestContext object (normally comes from the webflow)
	 */
	public void shutdownSession(RequestContext context) {
		// Get the HTTP session object
		HttpSession session = ((HttpServletRequest) context
				.getExternalContext().getNativeRequest()).getSession();
		// Invalidate the session object
		session.invalidate();
	}

	public List<MdSchema> getAvailableSchemas() {
		return this.mdFlow.getAvailableSchemas();
	}

	public List<MdSchemaInstance> getSelectedSchemas() {
		return this.mdFlow.getMdSchemaInstanceContainer();
	}

	public void setMdSchemaInstanceContainer(
			List<MdSchemaInstance> mdSchemaInstanceContainer) {
		this.mdFlow.setMdSchemaInstanceContainer(mdSchemaInstanceContainer);

	}

	public int getTypeSS() {
		return this.mdFlow.getTypeSS();
	}

	public boolean schemasSelected() {
		return this.mdFlow.schemasSelected();
	}

	public void clean() {
		this.mdFlow.clean();
	}

	public boolean isSchemasPresent() {
		return this.mdFlow.isSchemasPresent();
	}

	/**
	 * This method marks the elements that need to be displayed and that can be
	 * selected, depending on the type of template to be generated
	 * (collection-level or file-level)
	 * 
	 * @param mdSchemaInstance
	 *            the mdSchemaInstance object for the selected schema
	 */
	public void setSchemaHiddenElements(MdSchemaInstance mdSchemaInstance,
			int typeSS) throws IOException {
		ClassLoader classLoader = Thread.currentThread()
				.getContextClassLoader();
		InputStream is = null;
		JAXBContext context;

		try {
			if (typeSS == Constants.Q_COLLECTIONS_SS) {
				is = classLoader
						.getResourceAsStream("schemas/qudex-collection.xml");
			} else {
				is = classLoader
						.getResourceAsStream("schemas/qudex-document.xml");
			}
			context = JAXBContext.newInstance(Elements.class);
			Unmarshaller unmarshaller = context.createUnmarshaller();
			// Validate against schema, note: setting schema to null will turn
			// the validator off
			unmarshaller.setSchema(null);
			Elements schemaElements = Elements.class.cast(unmarshaller
					.unmarshal(is));
			for (Element elem : schemaElements.getElement()) {
				VocabularyElement vocElem = mdSchemaInstance
						.getElementFromSelectedSchemas(elem.getName());
				if (vocElem != null)
					vocElem.setHidden(false);
			}
		} catch (JAXBException e) {
			log.fatal(e.getMessage());
		} finally {
			if (is != null)
				is.close();
		}
	}
}
